/*
 * Copyright (c) 2020-2099 the original author or authors. All rights reserve.
 */
package org.tianyun.cloud.security.token.strategy;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.tianyun.cloud.security.configuration.SecurityProperties;
import org.tianyun.cloud.security.configuration.SecurityProperties.YamlJwtTokenStrategy;
import org.tianyun.cloud.security.token.common.AccessToken;
import org.tianyun.cloud.security.token.common.Authentication;
import org.tianyun.cloud.security.token.enhance.TokenSecretEnhance;
import org.tianyun.cloud.utils.CryptBuilder;
import org.tianyun.cloud.utils.DateUtils;
import org.tianyun.cloud.utils.JsonUtils;

import java.security.Principal;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * Jwt token strategy，token为jwt生成的密串，可以通过自定义密匙增强，改变randomKey来使Client Token失效
 *
 * <p>
 * <code>
 * iss: jwt签发者
 * sub: jwt所面向的用户
 * aud: 接收jwt的一方
 * exp: jwt的过期时间，这个过期时间必须要大于签发时间
 * nbf: 定义在什么时间之前，该jwt都是不可用的.
 * iat: jwt的签发时间
 * jti: jwt的唯一身份标识，主要用来作为一次性token,从而回避重放攻击。
 * </code>
 *
 * @auther ebert_chan
 */
public class JwtTokenStrategy implements TokenStrategy {

    private static final String JWT_PRIVATE_CLAIM = "v";

    private static final String SEPARATOR = ".";

    protected final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    private SecurityProperties securityProperties;

    private TokenSecretEnhance tokenSecretEnhance;

    /**
     * @param securityProperties
     */
    public JwtTokenStrategy(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }

    /*
     * @see org.tianyun.cloud.security.token.strategy.TokenStrategy#createAccessToken(org.tianyun.cloud.security.token.common.Authentication)
     */
    @Override
    public AccessToken createAccessToken(Authentication authentication) {
        Date now = DateUtils.now();
        Date expiresAt = null;

        if (securityProperties.getTokenStrategy().getAccessTokenValiditySeconds() != null && securityProperties.getTokenStrategy().getAccessTokenValiditySeconds() > 0) {
            expiresAt = DateUtils.addSeconds(new Date(), securityProperties.getTokenStrategy().getAccessTokenValiditySeconds());
        }

        Map<String, Object> header = new HashMap<>(2);
        header.put("typ", "JWT");
        header.put("alg", "HS256");

        YamlJwtTokenStrategy strategy = securityProperties.getTokenStrategy().getJwt();
        String rowValue = null;
        if (authentication.getRawValue() != null) {
            rowValue = JsonUtils.toJsonString(authentication.getRawValue());
            if (strategy.isEnableEncrypty()) {
                String nonce = String.valueOf(now.getTime());
                rowValue = CryptBuilder.aes().encrypt(nonce + SEPARATOR + rowValue, strategy.getKey(), strategy.getIv());
            }
        }

        Algorithm algorithm = Algorithm.HMAC256(getEnhanceKey(authentication, securityProperties.getTokenStrategy().getJwt().getSecret()));

        JWTCreator.Builder builder = JWT.create().withHeader(header).withIssuedAt(now).withSubject(authentication.getName());
        if (rowValue != null) {
            builder.withClaim(JWT_PRIVATE_CLAIM, rowValue);
        }
        if (expiresAt != null) {
            builder.withExpiresAt(expiresAt);
        }
        String value = builder.sign(algorithm);

        AccessToken accessToken = new AccessToken(value, authentication.getRawValue());
        if (expiresAt != null) {
            accessToken.setExpiresIn(securityProperties.getTokenStrategy().getAccessTokenValiditySeconds());
            accessToken.setExpiration(expiresAt);
        }

        return accessToken;
    }

    /*
     * @see org.tianyun.cloud.security.token.strategy.TokenStrategy#restoreRawValue(org.tianyun.cloud.security.token.common.Authentication)
     */
    @Override
    public void restoreRawValue(Authentication authentication) {
        throw new UnsupportedOperationException("JWT token does not support this operation.");
    }

    /*
     * @see org.tianyun.cloud.security.token.strategy.TokenStrategy#getAccessToken(java.security.Principal)
     */
    @Override
    public AccessToken getAccessToken(Principal principal) {
        throw new UnsupportedOperationException("JWT token does not support this operation.");
    }

    /*
     * @see org.tianyun.cloud.security.token.strategy.TokenStrategy#removeAccessToken(java.security.Principal)
     */
    @Override
    public void removeAccessToken(Principal principal) {
        throw new UnsupportedOperationException("JWT token does not support this operation.");
    }

    /*
     * @see org.tianyun.cloud.security.token.strategy.TokenStrategy#readAccessToken(java.lang.String)
     */
    @Override
    public AccessToken readAccessToken(String value) {
        AccessToken accessToken = null;
        try {
            YamlJwtTokenStrategy strategy = securityProperties.getTokenStrategy().getJwt();

            DecodedJWT jwt = JWT.decode(value);
            String subject = jwt.getSubject();

            Algorithm algorithm = Algorithm.HMAC256(getEnhanceKey(() -> subject, securityProperties.getTokenStrategy().getJwt().getSecret()));

            JWT.require(algorithm).build().verify(jwt);

            Date iat = jwt.getIssuedAt();
            Date exp = jwt.getExpiresAt();

            Integer expiresIn = null;
            if (exp != null && iat != null) {
                expiresIn = (int) ((exp.getTime() - iat.getTime()) / 1000);
            }

            Claim claim = jwt.getClaim(JWT_PRIVATE_CLAIM);
            String rowValue = null;
            if (claim != null) {
                rowValue = claim.asString();
            }

            if (StringUtils.hasLength(rowValue)) {
                if (!rowValue.startsWith("{")) {
                    if (strategy.isEnableEncrypty()) {
                        rowValue = CryptBuilder.aes().decrypt(rowValue, strategy.getKey(), strategy.getIv());
                        if (rowValue == null) {
                            LOGGER.info("invalid token: {}", value);
                            return accessToken;
                        }
                        int index = rowValue.indexOf(SEPARATOR);
                        if (index != -1) {
                            rowValue = rowValue.substring(rowValue.indexOf(SEPARATOR) + 1);
                        }
                    } else {
                        LOGGER.info("invalid token: {}", value);
                        return accessToken;
                    }
                }
            }

            accessToken = new AccessToken(value, null, expiresIn);
            accessToken.setExpiration(exp);
            if (StringUtils.hasLength(rowValue)) {
                accessToken.setRawValue(JsonUtils.parse(rowValue, Map.class));
            }

        } catch (Throwable e) {
            LOGGER.info("invalid token: {}", value);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("token verify failed", e);
            }
        }
        return accessToken;
    }

    public TokenSecretEnhance getTokenSecretEnhance() {
        return tokenSecretEnhance;
    }

    public void setTokenSecretEnhance(TokenSecretEnhance tokenSecretEnhance) {
        this.tokenSecretEnhance = tokenSecretEnhance;
    }

    /**
     * 获取增加的key
     *
     * @param principal
     * @param secret    原始key
     * @return
     * @auther ebert_chan
     */
    public String getEnhanceKey(Principal principal, String secret) {
        if (getTokenSecretEnhance() == null || !StringUtils.hasLength(principal.getName())) {
            return secret;
        }

        String randomKey = getTokenSecretEnhance().getRandomKey(principal);

        if (randomKey == null) {
            return secret;
        }

        return secret + randomKey;
    }

}
